/**
 * $Id: RadiusServer.java,v 1.11 2008/04/24 05:22:50 wuttke Exp $
 * Created on 09.04.2005
 *
 * @author Matthias Wuttke
 * @version $Revision: 1.11 $
 */
package org.tinyradius.util;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.tinyradius.attribute.RadiusAttribute;
import org.tinyradius.packet.AccessRequest;
import org.tinyradius.packet.AccountingRequest;
import org.tinyradius.packet.RadiusPacket;

public abstract class RadiusServer {

    private InetAddress listenAddress = null;
    private RadiusStat stat;
    private int authPort = 1812;
    private int acctPort = 1813;
    private DatagramSocket authSocket = null;
    private DatagramSocket acctSocket = null;
    private int socketTimeout = 3000;

    protected transient boolean closing = false;
    protected ExecutorService worker = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() - 1);
    private static Log logger = LogFactory.getLog(RadiusServer.class);


    public abstract String getSharedSecret(InetSocketAddress client);

    public abstract String getUserPassword(String userName);

    public void afterExecute(RadiusPacket request) {

    }


    /**
     * 认证消息处理
     *
     * @param accessRequest
     * @param client
     * @return
     * @throws RadiusException
     */
    public RadiusPacket accessRequestReceived(AccessRequest accessRequest, InetSocketAddress client) throws RadiusException {
        String plaintext = getUserPassword(accessRequest.getUserName());
        int type = RadiusPacket.ACCESS_REJECT;
        if (plaintext != null && accessRequest.verifyPassword(plaintext))
            type = RadiusPacket.ACCESS_ACCEPT;

        RadiusPacket answer = new RadiusPacket(type, accessRequest.getPacketIdentifier());
        copyProxyState(accessRequest, answer);
        return answer;
    }

    /**
     *  记账消息处理
     *
     * @param accountingRequest
     * @param client
     * @return
     * @throws RadiusException
     */
    public RadiusPacket accountingRequestReceived(AccountingRequest accountingRequest, InetSocketAddress client) throws RadiusException {
        RadiusPacket answer = new RadiusPacket(RadiusPacket.ACCOUNTING_RESPONSE, accountingRequest.getPacketIdentifier());
        copyProxyState(accountingRequest, answer);
        return answer;
    }

    /**
     * 认证监听
     */
    class AuthListen implements Runnable {
        public void run() {
            try {
                logger.info("starting RadiusAuthListener on port " + getAuthPort());
                listenAuth();
                logger.info("RadiusAuthListener is being terminated");
            } catch (Exception e) {
                e.printStackTrace();
                logger.fatal("auth thread stopped by exception", e);
            } finally {
                authSocket.close();
                logger.debug("auth socket closed");
            }
        }
    }

    /**
     * 计费监听
     */
    class AcctListen implements Runnable {
        public void run() {
            try {
                logger.info("starting RadiusAcctListener on port " + getAcctPort());
                listenAcct();
                logger.info("RadiusAcctListener is being terminated");
            } catch (Exception e) {
                e.printStackTrace();
                logger.fatal("acct thread stopped by exception", e);
            } finally {
                acctSocket.close();
                logger.debug("acct socket closed");
            }
        }
    }

    /**
     * 启动服务
     *
     * @param listenAuth
     * @param listenAcct
     */
    public void start(boolean listenAuth, boolean listenAcct) {

        if (listenAuth) {
            new Thread(new AuthListen(), "Radius Auth Listener").start();
        }
        if (listenAcct) {
            new Thread(new AcctListen(), "Radius Acct Listener").start();
        }

    }

    /**
     * Stops the server and closes the sockets.
     */
    public void stop() {
        logger.info("stopping Radius server");
        closing = true;
        if (authSocket != null)
            authSocket.close();
        if (acctSocket != null)
            acctSocket.close();
    }


    public int getAuthPort() {
        return authPort;
    }

    public void setAuthPort(int authPort) {
        if (authPort < 1 || authPort > 65535)
            throw new IllegalArgumentException("bad port number");
        this.authPort = authPort;
        this.authSocket = null;
    }

    public int getSocketTimeout() {
        return socketTimeout;
    }

    public void setSocketTimeout(int socketTimeout) throws SocketException {
        if (socketTimeout < 1)
            throw new IllegalArgumentException("socket tiemout must be positive");
        this.socketTimeout = socketTimeout;
        if (authSocket != null)
            authSocket.setSoTimeout(socketTimeout);
        if (acctSocket != null)
            acctSocket.setSoTimeout(socketTimeout);
    }

    public void setAcctPort(int acctPort) {
        if (acctPort < 1 || acctPort > 65535)
            throw new IllegalArgumentException("bad port number");
        this.acctPort = acctPort;
        this.acctSocket = null;
    }

    public int getAcctPort() {
        return acctPort;
    }


    public InetAddress getListenAddress() {
        return listenAddress;
    }


    public void setListenAddress(InetAddress listenAddress) {
        this.listenAddress = listenAddress;
    }

    protected void copyProxyState(RadiusPacket request, RadiusPacket answer) {
        List proxyStateAttrs = request.getAttributes(33);
        for (Iterator i = proxyStateAttrs.iterator(); i.hasNext(); ) {
            RadiusAttribute proxyStateAttr = (RadiusAttribute) i.next();
            answer.addAttribute(proxyStateAttr);
        }
    }


    protected void listenAuth() throws SocketException {
        listen(getAuthSocket());
    }


    protected void listenAcct() throws SocketException {
        listen(getAcctSocket());
    }


    protected void listen(final DatagramSocket s) {
        final DatagramPacket packetIn = new DatagramPacket(new byte[RadiusPacket.MAX_PACKET_LENGTH], RadiusPacket.MAX_PACKET_LENGTH);
        while (true) {

            try {
                // receive packet
                try {
                    logger.trace("about to call socket.receive()");
                    s.receive(packetIn);
                    if (logger.isDebugEnabled())
                        logger.debug("receive buffer size = " + s.getReceiveBufferSize());
                } catch (SocketException se) {
                    if (closing) {
                        // end thread
                        logger.info("got closing signal - end listen thread");
                        return;
                    }
                    // retry s.receive()
                    logger.error("SocketException during s.receive() -> retry", se);
                    continue;
                }

                // check client
                final InetSocketAddress localAddress = (InetSocketAddress) s.getLocalSocketAddress();
                final InetSocketAddress remoteAddress = new InetSocketAddress(packetIn.getAddress(), packetIn.getPort());
                final String secret = getSharedSecret(remoteAddress);
                if (secret == null) {
                    if (logger.isInfoEnabled())
                        logger.info("ignoring packet from unknown client " + remoteAddress + " received on local address " + localAddress);
                    continue;
                }

                // parse packet
                final RadiusPacket request = makeRadiusPacket(packetIn, secret);


                if (logger.isDebugEnabled())
                    logger.debug("received packet from " + remoteAddress + " on local address " + localAddress + ": " + request);

                // handle packet
                logger.trace("about to call RadiusServer.handlePacket()");

                asyncHandlePacket(s, localAddress, remoteAddress, request, secret);


            } catch (SocketTimeoutException ste) {
                // this is expected behaviour
                logger.trace("normal socket timeout");
            } catch (IOException ioe) {
                // error while reading/writing socket
                logger.error("communication error", ioe);
            } catch (RadiusException re) {
                // malformed packet
                logger.error("malformed Radius packet", re);
            }
        }
    }

    /**
     * 同步消息处理
     *
     * @param localAddress
     * @param remoteAddress
     * @param request
     * @param sharedSecret
     * @return
     * @throws RadiusException
     * @throws IOException
     */
    protected RadiusPacket handlePacket(InetSocketAddress localAddress, InetSocketAddress remoteAddress, RadiusPacket request, String sharedSecret)
            throws RadiusException, IOException {
        RadiusPacket response = null;

        if (localAddress.getPort() == getAuthPort()) {
            if (request instanceof AccessRequest) {
                stat.addAuthAll();
                response = accessRequestReceived((AccessRequest) request, remoteAddress);
                if (response.getPacketType() == RadiusPacket.ACCESS_ACCEPT)
                    stat.addAuthAccept();
                else if (response.getPacketType() == RadiusPacket.ACCESS_REJECT)
                    stat.addAuthReject();
            } else {
                stat.addAuthAbandon();
                logger.error("unknown Radius packet type: " + request.getPacketType());
            }
        } else if (localAddress.getPort() == getAcctPort()) {
            if (request instanceof AccountingRequest) {
                stat.addAcctAll();
                AccountingRequest accreq = (AccountingRequest) request;
                response = accountingRequestReceived(accreq, remoteAddress);
                if (accreq.getAcctStatusType() == AccountingRequest.ACCT_STATUS_TYPE_START)
                    stat.addAcctStart();
                else if (accreq.getAcctStatusType() == AccountingRequest.ACCT_STATUS_TYPE_STOP)
                    stat.addAcctStop();
                else if (accreq.getAcctStatusType() == AccountingRequest.ACCT_STATUS_TYPE_INTERIM_UPDATE)
                    stat.addAcctUpdate();
                else if (accreq.getAcctStatusType() == AccountingRequest.ACCT_STATUS_TYPE_ACCOUNTING_ON)
                    stat.addAcctOn();
                else if (accreq.getAcctStatusType() == AccountingRequest.ACCT_STATUS_TYPE_ACCOUNTING_OFF)
                    stat.addAcctOff();
            } else {
                stat.addAcctAbandon();
                logger.error("unknown Radius packet type: " + request.getPacketType());
            }
        } else {
            // ignore packet on unknown port
        }


        return response;
    }


    /**
     * 消息处理
     *
     * @param localAddress
     * @param remoteAddress
     * @param request
     * @param sharedSecret
     * @return
     * @throws RadiusException
     * @throws IOException
     */
    protected void asyncHandlePacket(final DatagramSocket s, final InetSocketAddress localAddress, final InetSocketAddress remoteAddress, final RadiusPacket request, final String sharedSecret)
            throws RadiusException, IOException {
        worker.submit(new Runnable() {
            public void run() {
                try {
                    //process();
                    RadiusPacket response = handlePacket(localAddress, remoteAddress, request, sharedSecret);
                    if (response != null) {
                        if (logger.isDebugEnabled())
                            logger.debug("send message: " + response);
                        DatagramPacket packetOut = makeDatagramPacket(response, sharedSecret, remoteAddress.getAddress(), remoteAddress.getPort(), request);
                        s.send(packetOut);
                        afterExecute(request);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    logger.error("async message process error", e);
                }
            }
        });


    }

    protected DatagramSocket getAuthSocket() throws SocketException {
        if (authSocket == null) {
            if (getListenAddress() == null)
                authSocket = new DatagramSocket(getAuthPort());
            else
                authSocket = new DatagramSocket(getAuthPort(), getListenAddress());
            authSocket.setSoTimeout(getSocketTimeout());
        }
        return authSocket;
    }

    protected DatagramSocket getAcctSocket() throws SocketException {
        if (acctSocket == null) {
            if (getListenAddress() == null)
                acctSocket = new DatagramSocket(getAcctPort());
            else
                acctSocket = new DatagramSocket(getAcctPort(), getListenAddress());
            acctSocket.setSoTimeout(getSocketTimeout());
        }
        return acctSocket;
    }

    /**
     * 从radius请求报文创建radius数据包
     *
     * @param packet
     * @param secret
     * @param address
     * @param port
     * @param request
     * @return
     * @throws IOException
     */
    protected DatagramPacket makeDatagramPacket(RadiusPacket packet, String secret, InetAddress address, int port, RadiusPacket request)
            throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        packet.encodeResponsePacket(bos, secret, request);
        byte[] data = bos.toByteArray();

        DatagramPacket datagram = new DatagramPacket(data, data.length, address, port);
        return datagram;
    }

    /**
     * 从radius数据包创建radius报文
     *
     * @param packet
     * @param sharedSecret
     * @return
     * @throws IOException
     * @throws RadiusException
     */
    protected RadiusPacket makeRadiusPacket(DatagramPacket packet, String sharedSecret) throws IOException, RadiusException {
        ByteArrayInputStream in = new ByteArrayInputStream(packet.getData());
        return RadiusPacket.decodeRequestPacket(in, sharedSecret);
    }


    public void setRadiusStat(RadiusStat stat) {
        this.stat = stat;
    }

}
